;

// リファクタリングするぞ
// いらないコード削る
// userAgentとかはalex.coreに移動
// EntityClassのprototypeをハックできるように
// batchモジュールを分割できる？
// テストをさらに充実させる

var alex = alex || {};
alex.resource = (function(global) {
    var datasources = {}
    var batchCallbacks = {};
    var batchMode = false;
    var batchRequests = [];

    // boilerplate of entity class
    var Entity = {
    };

    // static methods for entity class
    Entity.staticMethods = {
	find: function(id, callback) {
	    var url = encodeURI(this.datasourceURL + "/~" + this.namespace + "/" + this.name + "/" + id);
	    var entityClass = this;
	    doRequest("GET", url, null, null,
		      function(status, result) {
			  if (alex.httpSuccess(status)) {
			      callback(new entityClass(result));
			  } else {
			      switch (status) {
			      case 404: // not found
				  callback(null); // 
				  break;
			      default:
				  callback(undefined, alex.resource.errors.SERVER_ERROR.getInstance({code: xhr.status}));
				  break;
			      }
			  }
		      });
	}
    };
    // instance methods for entity class
    Entity.instanceMethods = {
	populate: function(values){
	    for (var p in values) {
		this.set(p, values[p]);
	    }
	},
	propertyValues: function() {
	    var result = {};
	    var properties = this.entityClass.properties;
	    for (var i = 0, n = properties.length; i < n; i++) {
		var propertyDef = properties[i];
		var propName = propertyDef.name;
		var value = this.get(propName);
		result[propName] = value;
	    }
	    return result;
	},
	save: function(callback) {
	    var self = this;
	    var entityClass = this.entityClass;
	    var uri = "/~" + entityClass.namespace + "/" + entityClass.name;
	    var url = entityClass.datasourceURL + uri;
	    var id = this.get(entityClass.pkProperty.name);
	    var method = "POST";
	    if (id) {
		url += "/" + id;
		method = "PUT";
	    }
	    url = encodeURI(url);
	    entityRequest(this, method, uri, url, {}, function(status, result) {
		if (alex.httpSuccess(status)) {
		    self.populate(result);
		    callback(result);
		} else {
		    switch (status) {
		    case 404: // not found (when updated)
			callback(undefined, alex.resource.errors.ENTITY_NOT_FOUND);
			break;
		    case 409: // conflicted
			// TODO: conflict info
			callback(undefined, alex.resource.errors.CONFLICTED);
			break;
		    default:
			callback(undefined, alex.resource.errors.SERVER_ERROR.getInstance({code: status}));
			break;
		    }
		}
	    });
	},
	remove: function(callback, failWhenNotFound) {
	    var self = this;
	    var entityClass = this.entityClass;
	    var id = this.get(entityClass.pkProperty.name);
	    var uri = "/~" + entityClass.namespace + "/" + entityClass.name + "/" + id;
	    var url = encodeURI(entityClass.datasourceURL + uri);
	    entityRequest(this, "DELETE", uri, url, {}, function(status, result) {
		if (alex.httpSuccess(status)) {
		    callback(null);
		} else {
		    switch (status) {
		    case 404: // not found (when updated)
			if (failWhenNotFound)
			    callback(undefined, alex.resource.errors.ENTITY_NOT_FOUND);
			else
			    callback(null);
			break;
		    case 409: // conflicted
			// TODO: conflict info
			callback(undefined, alex.resource.errors.CONFLICTED);
			break;
		    default:
			callback(undefined, alex.resource.errors.SERVER_ERROR.getInstance({code: status}));
			break;
		    }
		}
	    });
	}
    };
    var EntityClassMaker = function(siteUrl, namespace, entityName, entityDef) {
	var entityClass = function(props) {
	    var properties = {};
	    this.set = function(propertyName, value){
		var propertyDef = entityDef.properties[propertyName];
		if (!propertyDef) {
		    throw new Error("property definition is not found:" + propertyName);
		}
		switch (propertyDef.type.toLowerCase()) {
		case "date":
		    if (value !== undefined && value !== null && typeof value != "number") {
			if (value instanceof Date) {
			    value = value.getTime();
			} else {
			    throw new Error("invalid date:" + value);
			}
		    }
		    break;
		case "integer":
		    if (value !== undefined && value !== null && typeof value != "number") {
			if (typeof value == "string") {
			    value = Number(value);
			} else {
			    throw new Error("invalid integer:" + value);
			}
		    }
		    break;
		case "float":
		    if (value !== undefined && value !== null && typeof value != "number") {
			if (typeof value == "string") {
			    value = Number(value);
			} else {
			    throw new Error("invalid float:" + value);
			}
		    }
		    break;
		}
		properties[propertyName] = value;
	    };
	    this.get = function(propertyName){
		var propertyDef = entityDef.properties[propertyName];
		if (!propertyDef) {
		    throw new Error("property definition is not found:" + propertyName);
		}
		if (propertyDef.type == "date") {
		    var millis = properties[propertyName];
		    if (millis !== 0 && !millis) {
			return null;
		    } else {
			return new Date(millis);
		    }
		}
		return properties[propertyName];
	    };
	    if (props) {
		this.populate(props);
	    }
	};
	entityClass.name = entityName;
	entityClass.namespace = namespace;
	entityClass.datasourceURL = siteUrl;
	alex.extend(entityClass, Entity.staticMethods);
	alex.extend(entityClass.prototype, Entity.instanceMethods);

	entityClass.prototype.entityClass = entityClass;
	var props = [];
	for (var propertyName in entityDef.properties) {
	    var propertyDef = entityDef.properties[propertyName];
	    var cloned = alex.clone(propertyDef);
	    cloned.name = propertyName;
	    props.push(cloned);
	    if (cloned.primaryKey) {
		entityClass.pkProperty = cloned;
	    }
	    // generate accessors
	    var p = alex.beans.upFirstLetter(propertyName);
	    entityClass.prototype["set" + p] = (function(prop) {
		return function(value) {
		    this.set(prop, value);
		};
	    })(propertyName);
	    entityClass.prototype["get" + p] = (function(prop){
		return function(){
		    var val = this.get(prop);
		    return val === undefined ? null : val;
		}
	    })(propertyName);
	}
	
	entityClass.properties = props;
	return entityClass;
    };
    // utility functions (private)
    function entity2RequestParams(entity) {
	var params = {};
	var properties = entity.entityClass.properties;
	for (var i = 0, n = properties.length; i < n; i++) {
	    var propDef = properties[i];
	    var propName = propDef.name;
	    var propVal = entity.get(propName);
	    if (propVal !== null) {
	        // conversion
		switch (propDef.type) {
		case "date":
		    propVal = propVal.getTime();
		    break;
		case "user":
		    propVal = alex.clone(propVal);
		    break;
		}
	    }
	    params[propName] = propVal;
	}
	return params;
    }
    function entity2RequestParamsStr(entity) {
	var params = entity2RequestParams(entity);
	var paramsArray = [];
	for (var paramName in params) {
	    var value = params[paramName];
	    if (value instanceof Array) {
		for (var n = 0; n < value.length; i++) {
		    paramsArray.push(encodeURIComponent(paramName) + "=" + encodeURIComponent(String(value[n])));
		}
	    } else if (value instanceof Date) {
		paramsArray.push(encodeURIComponent(paramName) + "=" + value.getTime());
	    } else {
		paramsArray.push(encodeURIComponent(paramName) + "=" + encodeURIComponent(String(value)));
	    }
	}
	return paramsArray.join("&");
    }
    function entityRequest(entity, method, uri,  url, headers, callback) {
	if (batchMode) {
	    var entry = {
		method: method,
		uri: uri,
		entity: entity,
		headers: headers,
		callback: callback
	    };
	    batchRequests.push(entry);
	} else {
	    doRequest(method, url, entity2RequestParamsStr(entity), headers, callback);
	}
    }
    function doRequest(method, url, reqBody, headers, callback) {
	var xhr = alex.resource.newXhr(url);
	xhr.open(method, url, true);
	xhr.onreadystatechanged = function() {
	    if (xhr.readyState != 4) {
		return;
	    }
	    var status, responseText;
	    try {
		status = xhr.status;
		responseText = xhr.responseText;
	    } catch (e) {
		status = 0;
		responseText = null;
	    }
	    var result = null;
	    if (responseText && responseText.length > 0) {
		result = eval("(" + responseText + ")");
	    }
	    callback(status, result);
	};
	xhr.send(reqBody);
    }
    var module = {
	Entity: Entity,
	buildNS: function(siteUrl, config) {
	    var namespaces = datasources[siteUrl];
	    if (!namespaces) {
		namespaces = datasources[siteUrl] = {};
	    }
	    var namespace = namespaces[config.namespace];
	    if (!namespace) {
		namespace = namespaces[config.namespace] = {};
	    }
	    for (var entityName in config.entities) {
		var entityDef = config.entities[entityName];
		var entityClass =  namespace[entityName] = EntityClassMaker(siteUrl, config.namespace, entityName, entityDef);
	    }
	    return namespace;
	},
	newXhr: function() {
	    return window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
	},
	batch: {
	    execute: function(fn, callback) {
		batchMode = true;
		var requests = [], callbacks = {};
		try {
		    fn();
		    var callbacks = {},requests = [];
		    for (var i = 0, n = batchRequests.length; i < n; i++) {
			var entry = batchRequests[i];
			var params = entity2RequestParams(entry.entity);
			var request = {
			    batchId: i + 1,
			    method: entry.method,
			    uri: entry.uri,
			    entity: params,
			    headers: entry.headers
			};
			requests.push(request);
			callbacks[request.batchId] = entry.callback;
		    }
		    var reqBody = JSON.stringify(requests);
		    doRequest("POST", url, reqBody, null,
			      function(status, results) {
				  if (alex.httpSuccess(status)) {
				      var result = {
					  hadError: false
				      };
				      for (var batchId in results) {
					  var opeResult = results[batchId];
					  var opeCallback = callbacks[batchId];
					  if (!alex.httpSuccess(opeResult.status.code)) {
					      result.hadError = true;
					  }
					  opeCallback(opeResult.status.code, opeResult.content);
				      }
				      callback(result);
				  } else {
				      callback(undefined, alex.resource.errors.SERVER_ERROR.getInstance({code: xhr.status}));
				  }
			      });
		} catch (e) {
		    callback(undefined, e);
		} finally {
		    batchOperations = [];
		    batchCallbacks = {};
		    batchMode = false;
		}
	    }
	}
    };
    return module;
})(this);

alex.resource.errors = alex.errors.define({
    ENTITY_NOT_FOUND: {code: 404},
    CONFLICTED: {code: 409},
    SERVER_ERROR: {}
});